import java.security.InvalidParameterException;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;

public class Game extends Canvas{
	GC gc;

	int mapWidth;
	int mapHeight;
	boolean periodicBorder;

	Point viewCenter;
	boolean cells[][];
	int zoom = 100;
	final static int zoomIncrement = 20;
	final static int zoomMin = 20;
	final static int zoomMax = 900;
	boolean paused = true;;
	int interval = 100;
	boolean firstPan = true;
	Point lastPan;

	final Runnable timer = new Runnable() {
        public void run() {
            if(paused) {
				return;
            }

            gameUpdate();

            getDisplay().timerExec(interval, this);
		}
	};

	final static Color BLACK = new Color(0, 0, 0);
	final static Color GREY = new Color(128, 128, 128);
	final static Color WHITE = new Color(255, 255, 255);
	final static int cellWidth = 10;

	public Game(Composite parent, int style) {
		super(parent, style);
		gc = new GC(this);
		gameRedraw();
		addListeners();
	}

	public Game(Composite parent, int style, int mapWidth, int mapHeight, boolean periodicBorder){
		super(parent, style);
		if(mapWidth <= 0 || mapHeight <= 0)
			throw new InvalidParameterException("Invalid map width or height. "
												+ "mapWidth: " + mapWidth
												+ ", mapHeight: " + mapHeight);
		this.mapWidth = mapWidth;
		this.mapHeight = mapHeight; 
		this.periodicBorder = periodicBorder;

		viewCenter = new Point(cellWidth*mapWidth/2, cellWidth*mapHeight/2);
		
		cells = new boolean[4+mapHeight][4+mapWidth];
		gc = new GC(this);
		addListeners();
	}

	public boolean set(boolean map[][], boolean periodicBorder) {
		if(map == null || map.length <= 4 || map[0].length <= 4)
			return false;
		for(boolean row[] : map)
			if(row.length != map[0].length)
				return false;
		cells = map;
		mapWidth = map[0].length-4;
		mapHeight = map.length-4;
		this.periodicBorder = periodicBorder;
		gameRedraw();
		return true;
	}

	public void switchPause() {
		if(cells == null)
			return;
		paused = !paused;
		if(!paused)
			getDisplay().timerExec(0, timer);
	}

	public boolean paused() {
		return paused;
	}

	
	public void setInterval(int interval) {
		if(interval <= 0)
			return;
		this.interval = interval;
	}

	/**
	 * Converts canvas coordinate to effective cell row and column. 
	 * Returns {-1, -1} if periodic border is set and the canvas coordinate 
	 * is out of the border of the map. 
	 * @param p Point on the canvas
	 * @return
	 * If periodic border is enabled and the canvas coordinate is outside of
	 * the border of the map, returns a Point whose x and y are -1. 
	 * Returns a point containing the row and column of corresponding
	 * cell, otherwise. 
	 */
	Point viewToCell(Point p) {
		int c = (int)(((p.x-(double)getSize().x/2) * 100/zoom + viewCenter.x) / cellWidth);
		int r = (int)(((p.y-(double)getSize().y/2) * 100/zoom + viewCenter.y) / cellWidth);
		if(!periodicBorder) {
			if(c < 0 || c >= mapWidth || r < 0 || r >= mapHeight)
				return new Point(-1, -1);
		}
		return new Point(c, r);
	}

	Point cellToView(int r, int c) {
		int x = (int)((c*cellWidth - viewCenter.x) * ((double)zoom/100) + (double)getSize().x/2);
		int y = (int)((r*cellWidth - viewCenter.y) * ((double)zoom/100) + (double)getSize().y/2);
		return new Point(x, y);
	}

	Rectangle cellToViewRect(int r, int c) {
		Point p = cellToView(r, c);
		int width = (int)(cellWidth * (double)zoom/100);
		return new Rectangle(p.x, p.y, width, width);
	}

	void drawCell(int r, int c) {
		if(cells == null)
			return;
		drawCell(r, c, false, cells[2+r][2+c]);
	}

	void drawCell(int r, int c, boolean drawBorder){
		if(cells == null)
			return;
		drawCell(r, c, drawBorder, cells[2+r][2+c]);
	}

	void drawCell(int r, int c, boolean drawBorder, boolean state) {
		if(cells == null)
			return;
		gc.setBackground(state ? BLACK : WHITE);
		gc.setForeground(GREY);
		Rectangle rect = cellToViewRect(r, c);
		if(!rect.intersects(new Rectangle(0, 0, getSize().x, getSize().y)))
			return;
		gc.fillRectangle(rect);
		if(drawBorder)
			gc.drawRectangle(rect);

		if(periodicBorder) {
			Point directions[] = {new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1)};
			for(Point direction : directions) {
				int rPortal = r + mapHeight*direction.y;
				int cPortal = c + mapHeight*direction.x;
				Rectangle rectPortal = cellToViewRect(rPortal, cPortal);
				Point size = getSize();
				Rectangle view = new Rectangle(0, 0, size.x, size.y);
				while(rectPortal.intersects(view)) {
					gc.fillRectangle(rectPortal);
					gc.drawRectangle(rectPortal);
					rPortal += mapHeight*direction.y;
					cPortal += mapHeight*direction.x;
					rectPortal = cellToViewRect(rPortal, cPortal);
				}
			}
		}
	}

	boolean[][] compute() {
		if(cells == null)
			return null;
		boolean newCells[][] = new boolean[cells.length][cells[0].length];
		Point directions[] = {
				new Point(-1, -1), new Point( 0, -1), new Point( 1, -1), 
				new Point(-1,  0),					  new Point( 1,  0), 
				new Point(-1,  1), new Point( 0,  1), new Point( 1,  1)
		};
		for(int r = periodicBorder?1:2; r < 2+mapHeight+(periodicBorder?1:0); r++) {
			for(int c = periodicBorder?1:2; c < 2+mapWidth+(periodicBorder?1:0); c++) {
				int cnt = 0;
				for(Point direction : directions) {
					if(cells[r+direction.y][c+direction.x])
						cnt++;
				}
				newCells[r][c] = cells[r][c] && (cnt == 2 || cnt == 3)
							   || !cells[r][c] && cnt == 3;
			}
		}
		if(periodicBorder) {
			// Portal features here
		}
		return newCells;
	}

	void gameUpdate() {
		boolean newCells[][] = compute();
		for(int r = 2; r < 2+mapHeight; r++) {
			for(int c = 2; c < 2+mapWidth; c++) {
				if(newCells[r][c] != cells[r][c]) {
					drawCell(r-2, c-2, true, newCells[r][c]);
				}
			}
		}
		cells = newCells;
	}

	public void gameRedraw() {
		if(cells == null) {
			gc.setBackground(GREY);
			gc.fillRectangle(getClientArea());
			return;
		}
		if(!periodicBorder) {
			gc.setBackground(WHITE);
			gc.fillRectangle(0, 0, getSize().x, getSize().y);
		}
		for(int r = 2; r < cells.length-2; r++) {
			for(int c = 2; c < cells[r].length-2; c++) {
				drawCell(r-2, c-2, false);
			}
		}
		for(int c = 0; c < mapWidth; c++) {
			Point p1 = cellToView(0, c);
			Point p2 = cellToView(mapHeight, c);
			gc.drawLine(p1.x, p1.y, p2.x, p2.y);
		}
		for(int r = 0; r < mapHeight; r++) {
			Point p1 = cellToView(r, 0);
			Point p2 = cellToView(r, mapWidth);
			gc.drawLine(p1.x, p1.y, p2.x, p2.y);
		}
		gc.setForeground(GREY);
		Point p1 = cellToView(0, 0);
		Point p2 = cellToView(mapHeight, mapWidth);
		Rectangle outerBorder = new Rectangle(p1.x, p1.y, p2.x-p1.x, p2.y-p1.y);
		gc.drawRectangle(outerBorder);
		outerBorder.x++; outerBorder.y++;
		outerBorder.width -= 2; outerBorder.height -= 2;
		gc.drawRectangle(outerBorder);
	}

	void addListeners() {
		addKeyListener(new KeyAdapter() {
			public void keyReleased(KeyEvent e) {
				if(cells == null)
					return;
				if(e.keyCode == SWT.SPACE) {
					paused = !paused;
					if(!paused) {
						getDisplay().timerExec(0, timer);
					}
					Event event = new Event();
					event.data = paused;
					notifyListeners(SWT.SetData, event);
				}
			}
		});
		addMouseWheelListener(new MouseWheelListener() {
			public void mouseScrolled(MouseEvent e) {
				if(cells == null)
					return;
				zoom += zoomIncrement * (e.count>0 ? 1 : -1);
				if(zoom < zoomMin)
					zoom = zoomMin;
				else if(zoom > zoomMax)
					zoom = zoomMax;
				gameRedraw();
			}
		});
		addMouseListener(new MouseAdapter() {
			public void mouseDown(MouseEvent e) {
				if(cells == null)
					return;
				if(e.button == 1 || e.button == 3) {
					Point cell = viewToCell(new Point(e.x, e.y));
					if(!periodicBorder && cell.x == -1 && cell.y == -1)
						return;
					cells[2+cell.y][2+cell.x] = e.button==1;
					drawCell(cell.y, cell.x, true);
				}
			}
		});
		addMouseMoveListener(new MouseMoveListener() {
			public void mouseMove(MouseEvent e) {
				if(cells == null)
					return;
				if((e.stateMask&(SWT.BUTTON1 | SWT.BUTTON3)) != 0) {
					Point cell = viewToCell(new Point(e.x, e.y));
					if(!periodicBorder && cell.x == -1 && cell.y == -1)
						return;
					cells[2+cell.y][2+cell.x] = (e.stateMask & SWT.BUTTON1) != 0;
					drawCell(cell.y, cell.x, true);
				}
				if((e.stateMask & SWT.ALT) != 0) {
					if(lastPan != null) {
						viewCenter = new Point((int)(viewCenter.x+(lastPan.x-e.x)*(double)100/zoom)
											 , (int)(viewCenter.y+(lastPan.y-e.y)*(double)100/zoom));
						gameRedraw();
					}
					lastPan = new Point(e.x, e.y);
				} else {
					lastPan = null;
				}
			}
		});
	}

	public int getMapWidth() {
		return mapWidth;
	}

	public int getMapHeight() {
		return mapHeight;
	}

	public boolean getPeriodicBorder() {
		return periodicBorder;
	}

	public boolean[][] getCells(){
		return cells.clone();
	}

	public void load(int mapWidth, int mapHeight, boolean periodicBorder, byte[] raw) throws IllegalArgumentException {
		if(raw.length != (mapWidth+4)*(mapHeight+4))
			throw new IllegalArgumentException("Length of parameter raw doesn't meet requirement. Length: "+raw.length);
		cells = new boolean[mapHeight+4][mapWidth+4];
		for(int i = 0; i < cells.length; i++)
			for(int j = 0; j < cells[i].length; j++)
				cells[i][j] = raw[(mapWidth+4)*i + j] == 0 ? false : true;
		this.mapWidth = mapWidth;
		this.mapHeight = mapHeight;
		this.periodicBorder = periodicBorder;
		gameRedraw();
	}
}
